出处:掘金

原作者:ErpanOmer


我们每天的开发,可能都是从一个 npm run dev 开始的。npm scripts 对我们来说,天天用它,但很少去思考它

不信,你看看你项目里的 package.json,是不是长这样:

"scripts": {
  "dev": "vite",
  "build": "rm -rf dist && tsc && vite build", // 嘿,眼熟吗?
  "lint": "eslint .",
  "lint:fix": "eslint . --fix",
  "test": "vitest",
  "test:watch": "vitest --watch",
  "preview": "vite preview"
}

这能用吗?当然能用

但这专业吗?在我看来,未必!

一个好的 scripts,应该是原子化的、跨平台的。而上面这个,一个 build 命令就不行,而且 rm -rf 在 Windows 上还得装特定环境才能跑

今天,我就来聊聊,如何用 prepost--,把你的脚本,升级成专业的脚本

prepost:命令的生命周期钩子

prepost,是 npm 内置的一种钩子机制

它的规则很简单:

这个自动的特性,就是神一般的存在

我们来改造那个前面👆提到的 build 脚本

业余写法:用 && 手动编排

"scripts": {
  "clean": "rimraf dist", // rimraf 解决跨平台删除问题
  "lint": "eslint .",
  "build:tsc": "tsc",
  "build:vite": "vite build",
  "build": "npm run clean && npm run lint && npm run build:tsc && npm run build:vite"
}

你的 build 脚本,它必须记住所有的前置步骤。如果哪天你想在 build 前,再加一个 test,还得去修改 build 的定义。这违反了单一职责

专业写法:用 pre 自动触发

"scripts": {
  "clean": "rimraf dist",
  "lint": "eslint .",
  "test": "vitest run",
  "build:tsc": "tsc",
  "build:vite": "vite build",

  // build的前置钩子
  "prebuild": "npm run clean && npm run lint && npm run test", 
  
  // build的核心命令
  "build": "npm run build:tsc && npm run build:vite",
  
  // build的后置钩子
  "postbuild": "echo 'Build complete! Check /dist folder.'"
}

看到区别了吗?

现在,当我只想构建时,我依然执行 npm run build

npm 会自动帮我执行 prebuild(清理、Lint、测试)→ 然后执行 build(编译、打包)→ 最后执行 postbuild(打印日志)

我的 build 脚本,只关心构建这件事。而 prebuild 脚本,只关心前置检查这件事

这就是单一职责和关注点分离

你甚至可以利用这个特性,搞点骚操作😁:

"scripts": {
  // 当你执行 npm start 时,它会自动先执行 npm run build
  "prestart": "npm run build", 
  "start": "node dist/server.js"
}

-- 双短线:脚本参数

-- 是我最爱的一个特性。它是一个参数分隔符

它的作用是:告诉 npm,我的 npm 参数到此为止了,后面所有的东西,都原封不动地,传给我要执行的那个底层命令

我们来看开头👆那个脚本:

"scripts": {
  "test": "vitest",
  "test:watch": "vitest --watch"
}

为了一个 --watch 参数,你复制了一个几乎一模一样的脚本。如果明天你还想要 --coverage 呢?再加一个 test:coverage?这叫垃圾代码💩

专业写法:用 -- 动态传参

"scripts": {
  "test": "vitest"
}

就这一行,够了

等等,那我怎么跑 watchcoverage

答案,就是用 --

# 1. 只跑一次
$ npm run test -- --run
# 实际执行: vitest --run

# 2. 跑 watch 模式
$ npm run test -- --watch
# 实际执行: vitest --watch

# 3. 跑覆盖率
$ npm run test -- --coverage
# 实际执行: vitest --coverage

# 4. 跑某个特定文件
$ npm run test -- src/my-component.test.ts
# 实际执行: vitest src/my-component.test.ts

-- 就像一个参数隧道 ,它把你在命令行里,跟在 -- 后面的所有参数,原封不动地扔给了 vitest 命令

一个专业的 CI/CD 脚本

好了,我们把 pre / post-- 结合起来,看看一个专业的 package.json 是长什么样子:

"scripts": {
  // 1. Lint
  "lint": "eslint .",
  "lint:fix": "eslint . --fix",

  // 2. Test
  "test": "vitest",
  "pretest": "npm run lint", // 在test前,必须先lint

  // 3. Build
  "build": "tsc && vite build",
  "prebuild": "npm run test -- --run", // 在build前,必须先test通过
  
  // 4. Publish (发布的前置钩子)
  // prepublishOnly 是一个 npm 内置的、比 prepublish 更安全的钩子
  // 它只在 npm publish 时执行,而在 npm install 时不执行
  "prepublishOnly": "npm run build" // 在发布前,必须先build
}

看看我们构建了怎样一条自动化脚本:

  1. 你兴高采烈地敲下 npm publish,准备发布
  2. npm 一看,有个 prepublishOnly,于是它先去执行 npm run build
  3. npm 一看,build 有个 prebuild,于是它又先去执行 npm run test -- --run
  4. npm 一看,test 有个 pretest,于是它又双叒叕先去执行 npm run lint

最终的执行流是:LintTestBuildPublish

这些脚本,被 pre 钩子,自动地、强制地串联了起来。你作为开发者,根本没有机会犯错。你不可能发布一个连 Lint 都没过或者测试未通过的包

写在后面

npm scripts,它不是一个简单的脚本快捷方式。它是一个工作流(Workflow)的定义

prepost,定义了你工作流的执行顺序依赖,保证了代码检查等功能,而 -- 是确保你工作流中的脚本参数